﻿/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using DaveSexton.DocToMaml.Extensions;

namespace DaveSexton.DocToMaml.Maml
{
	internal enum MamlElementType
	{
		Default, 
		Summary,
		Introduction,
		Section,
		SectionTitle,
		SectionContent,
		RelatedTopics
	}

	internal abstract class MamlDocumentBase
	{
		#region Public Properties
		public abstract string DefaultContainerElementName { get; }

		public int ProcessedImageCount { get; private set; }

		public int ProcessedLinkCount { get; private set; }
		#endregion

		#region Private / Protected
		private readonly RuleCollection removableTagIds, removableTagClasses, mamlElementTagIds, mamlElementTagClasses, mamlElementTagNames;
		private readonly ConceptualImageManager images;
		private readonly FileGuidManager guids;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="MamlDocumentBase" /> class for derived classes.
		/// </summary>
		public MamlDocumentBase(IRuleContainer rules, ConceptualImageManager imageManager, FileGuidManager guidManager)
		{
			if (rules == null)
				throw new ArgumentNullException("rules");

			if (imageManager == null)
				throw new ArgumentNullException("imageManager");

			if (guidManager == null)
				throw new ArgumentNullException("guidManager");
			
			this.images = imageManager;
			this.guids = guidManager;

			removableTagIds = rules.GetRules(RuleAction.Remove, RuleMatchKind.ElementId);
			removableTagClasses = rules.GetRules(RuleAction.Remove, RuleMatchKind.StyleSheetClass);
			mamlElementTagIds = rules.GetRules(RuleAction.Convert, RuleMatchKind.ElementId);
			mamlElementTagClasses = rules.GetRules(RuleAction.Convert, RuleMatchKind.StyleSheetClass);
			mamlElementTagNames = rules.GetRules(RuleAction.Convert, RuleMatchKind.TagName);
		}
		#endregion

		#region Methods
		public virtual void Reset()
		{
			ProcessedImageCount = 0;
			ProcessedLinkCount = 0;
		}

		public void ConvertFromHtml(Guid guid, XmlReader reader, XmlWriter writer)
		{
			Reset();

			while (reader.Read())
			{
				if (reader.Name == "body")
				{
					using (XmlReader bodyReader = reader.ReadSubtree())
					{
						ParseHtmlBody(bodyReader);
					}
				}
			}

			StringBuilder firstPassOutput = new StringBuilder();

			using (XmlWriter firstPassWriter = XmlWriter.Create(firstPassOutput, writer.Settings))
			{
				WriteMamlDocument(firstPassWriter, guid);
			}

			using (System.IO.StringReader secondPassInput = new System.IO.StringReader(firstPassOutput.ToString()))
			{
				using (XmlReader firstPassReader = XmlReader.Create(secondPassInput, reader.Settings))
				{
					CleanMamlDocument(firstPassReader, writer);
				}
			}
		}

		protected abstract void CleanMamlDocument(XmlReader reader, XmlWriter writer);

		protected abstract void WriteMamlDocument(XmlWriter writer, Guid guid);

		private void ParseHtmlBody(XmlReader reader)
		{
			reader.MoveToContent();
			reader.Read();	// skip body start tag

			while (!reader.EOF)
			{
				if (reader.NodeType == XmlNodeType.Element)
				{
					string id = reader.GetAttribute("id");

					if (string.IsNullOrEmpty(id) || !removableTagIds.Contains(id))
					{
						string @class = reader.GetAttribute("class");

						if (string.IsNullOrEmpty(@class) || !removableTagClasses.Contains(@class))
							ParseHtmlElement(id, @class, true, reader);
						else
							reader.Skip();
					}
					else
						reader.Skip();
				}
				else
					reader.Skip();
			}
		}

		protected bool ShouldIgnoreElement(XmlReader reader)
		{
			if (reader.NodeType == XmlNodeType.Element)
			{
				string id = reader.GetAttribute("id");

				if (!string.IsNullOrEmpty(id) && removableTagIds.Contains(id))
					return true;

				string @class = reader.GetAttribute("class");

				if (!string.IsNullOrEmpty(@class) && removableTagClasses.Contains(@class))
					return true;
			}

			return false;
		}

		private bool ParseHtmlElement(string id, string @class, bool inOuterContext, XmlReader reader)
		{
			if (mamlElementTagNames.Contains(reader.LocalName))
				return ParseHtmlElementIntoMamlElement(reader, (MamlElementType) mamlElementTagNames[reader.LocalName].Value, inOuterContext);
			else if (!string.IsNullOrEmpty(id) && mamlElementTagIds.Contains(id))
				return ParseHtmlElementIntoMamlElement(reader, (MamlElementType) mamlElementTagIds[id].Value, inOuterContext);
			else if (!string.IsNullOrEmpty(@class) && mamlElementTagClasses.Contains(@class))
				return ParseHtmlElementIntoMamlElement(reader, (MamlElementType) mamlElementTagClasses[@class].Value, inOuterContext);
			else
				return ParseHtmlElementIntoMamlElement(reader, MamlElementType.Default, inOuterContext);
		}

		private bool ParseHtmlElement(bool parseDefault, XmlReader reader)
		{
			return ParseHtmlElement(reader.GetAttribute("id"), reader.GetAttribute("class"), parseDefault, reader);
		}

		protected abstract bool ParseHtmlElementIntoMamlElement(XmlReader reader, MamlElementType element, bool inOuterContext);

		protected string ReadElementAsString(XmlReader reader, bool collapseWhitespce)
		{
			StringBuilder builder = new StringBuilder();

			using (XmlReader subtree = reader.ReadSubtree())
			{
				subtree.MoveToContent();

				do
				{
					builder.Append(subtree.ReadString());

					if (subtree.NodeType == XmlNodeType.EndElement || subtree.IsEmptyElement)
						subtree.Read();

					if (ShouldIgnoreElement(subtree))
						subtree.Skip();
				}
				while (!subtree.EOF);
			}

			if (collapseWhitespce)
				return CollapseWhitespace(builder.ToString());
			else
				return builder.ToString();
		}

		protected static string CollapseWhitespace(string text)
		{
			if (text == null)
				return string.Empty;

			StringBuilder builder = new StringBuilder();
			StringBuilder runningWhitespace = new StringBuilder(32);
			int i = 0;

			foreach (char c in text)
			{
				if (char.IsWhiteSpace(c))
					runningWhitespace.Insert(i++, c);
				else
				{
					if (i > 0)
					{
						if (builder.Length > 0)
							builder.Append(" ");

						i = 0;
					}

					builder.Append(c);
				}
			}

			if (i > 0 && builder.Length > 0)
				builder.Append(" ");

			return builder.ToString();
		}

		protected string ReadAsMamlContent(XmlReader reader, bool containerRequired)
		{
			StringBuilder output = new StringBuilder();

			XmlWriterSettings settings = new XmlWriterSettings();
			settings.ConformanceLevel = ConformanceLevel.Fragment;
			settings.Indent = true;
			// NOTE: Encoding will always be Unicode when writing to a StringBuilder

			using (XmlWriter writer = XmlWriter.Create(output, settings))
			{
				WriteAsMamlContent(reader, writer, containerRequired);
			}

			reader.Read();

			return output.ToString();
		}

		private void WriteAsMamlContent(XmlReader reader, XmlWriter writer, bool containerRequired)
		{
			bool hasOuterContainer = false;

			WriteAsMamlContent(reader, writer, containerRequired, true, ref hasOuterContainer);

			if (hasOuterContainer)
				writer.WriteEndElement();
			else if (containerRequired)
				writer.WriteElementString(DefaultContainerElementName, string.Empty);
		}

		private void WriteAsMamlContent(XmlReader outerReader, XmlWriter writer, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			using (XmlReader reader = outerReader.ReadSubtree())
			{
				reader.MoveToContent();
				reader.Read();

				while (!reader.EOF)
				{
					switch (reader.NodeType)
					{
						case XmlNodeType.Element:
							if (ShouldIgnoreElement(reader))
								reader.Skip();
							else if (!ParseHtmlElement(false, reader))
							{
								WriteMamlElement(reader, writer, containerRequired, canCreateNewContainers, ref hasOuterContainer);
								reader.Read();
							}
							break;
						default:
							string text = reader.ReadString();

							if (reader.NodeType == XmlNodeType.EndElement || reader.IsEmptyElement)
								reader.Read();

							if (text.Trim().Length > 0)
							{
								if (containerRequired && !hasOuterContainer && canCreateNewContainers)
								{
									writer.WriteStartElement(DefaultContainerElementName);

									hasOuterContainer = true;
								}

								writer.WriteString(Environment.NewLine + CollapseWhitespace(text));
							}
							break;
					}
				}
			}
		}

		private void WriteMamlElement(XmlReader reader, XmlWriter writer, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			// NOTE: This method must not advance the reader past the end tag of the current element.

			switch (reader.LocalName)
			{
				// NOTE: During HTML-to-XML conversion, a rule can replace any HTML element with a different tag name, such as para or image.
				// For this reason various MAML tags are matched here as well (like para and image).

				case "div":
				case "fieldset":
				case "p":
				case "para":		// MAML tag
					WriteDefaultContainer(reader, writer, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				// headers: 
				case "h1":
				case "h2":
				case "h3":
				case "h4":
				case "h5":
				case "h6":
					// these HTML tags are reserved for future use; for now, just convert them to normal containers
					WriteDefaultContainer(reader, writer, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "ul":
					WriteMamlList(reader, writer, false, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "ol":
					WriteMamlList(reader, writer, true, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "table":
					WriteMamlTable(reader, writer, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "blockquote":
				case "quote":				// MAML tag
					WriteMamlQuote(reader, writer, false, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "q":
				case "quoteInline":		// MAML tag
					WriteMamlQuote(reader, writer, true, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "sup":
				case "superscript":		// MAML tag
					WriteMamlInlineElement(reader, writer, "superscript", containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "sub":
				case "subscript":			// MAML tag
					WriteMamlInlineElement(reader, writer, "subscript", containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				case "a":
				case "externalLink":		// MAML tag
					if (canCreateNewContainers)
						EnsureContainer(writer, null, containerRequired, ref hasOuterContainer);

					WriteMamlLink(reader, writer);
					break;
				case "img":
				case "image":			// MAML tag
					if (canCreateNewContainers)
						EnsureContainer(writer, null, containerRequired, ref hasOuterContainer);

					WriteMamlImage(reader.GetAttribute("id"), reader.GetAttribute("src"), reader.GetAttribute("title"), writer);
					break;
				// font formatting tags:
				case "b":
				case "big":
				case "font":
				case "i":
				case "s":
				case "small":
				case "strike":
				case "tt":
				case "u":
				// other semantics:
				case "center":
				case "del":
				case "ins":
				case "span":
					// ignore these tags but keep their content
					WriteAsMamlContent(reader, writer, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				// phrases: 
				case "abbr":
				case "acronym":
				case "address":
				case "cite":
				case "em":
				case "legend":
				case "strong":
				case "phrase":		// MAML tag
					WriteMamlInlineElement(reader, writer, "phrase", containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				// code block:
				case "pre":
				case "code":			// MAML tag (and HTML)
					WriteMamlCode(reader, writer, false, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				// in-line code: 
				case "samp":
				case "var":
				case "codeInline":		// MAML tag
					WriteMamlCode(reader, writer, true, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				// definitions: 
				case "dfn":
				case "dt":
				case "dd":
					// these HTML tags are reserved for future use; for now, just continue parsing their content.
					WriteAsMamlContent(reader, writer, containerRequired, canCreateNewContainers, ref hasOuterContainer);
					break;
				default:
					// ignore unmatched elements
					break;
			}
		}

		private void WriteDefaultContainer(XmlReader reader, XmlWriter writer, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers)
				EnsureContainer(writer, DefaultContainerElementName, containerRequired, ref hasOuterContainer);

			WriteAsMamlContent(reader, writer, false, false, ref hasOuterContainer);
		}

		protected void WriteMamlLink(XmlReader reader, XmlWriter writer)
		{
			ReadHtmlAnchor(reader).Write(writer);

			ProcessedLinkCount = ProcessedLinkCount + 1;
		}

		protected MamlLink ReadHtmlAnchor(XmlReader reader)
		{
			string href = reader.GetAttribute("href");
			string alt = reader.GetAttribute("alt");
			string text = ReadElementAsString(reader, true);

			Guid guid = guids.GetGuid(href);

			if (guid != Guid.Empty)
				href = guid.ToString() + ".htm";

			return new MamlLink(MamlLinkType.ExternalLink, href, text, alt);
		}

		private void WriteMamlQuote(XmlReader reader, XmlWriter writer, bool forceInline, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			bool startTagWritten = false;

			if (canCreateNewContainers)
			{
				EnsureContainer(writer, (forceInline) ? null : "quote", containerRequired, ref hasOuterContainer);

				if (forceInline)
				{
					writer.WriteStartElement("quoteInline");
					startTagWritten = true;
				}
			}
			else
			{
				writer.WriteStartElement("quoteInline");
				startTagWritten = true;
			}

			WriteAsMamlContent(reader, writer, false, false, ref hasOuterContainer);

			if (startTagWritten)
				writer.WriteEndElement();
		}

		private void WriteMamlCode(XmlReader reader, XmlWriter writer, bool forceInline, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers)
			{
				if (forceInline)
					EnsureContainer(writer, null, containerRequired, ref hasOuterContainer);
				else
				{
					EnsureContainer(writer, "code", containerRequired, ref hasOuterContainer);

					writer.WriteString(ReadElementAsString(reader, false));
					return;
				}
			}

			WriteMamlInlineElementString(reader, writer, "codeInline", containerRequired, canCreateNewContainers, ref hasOuterContainer);
		}

		private void WriteMamlInlineElement(XmlReader reader, XmlWriter writer, string elementName, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers)
				EnsureContainer(writer, null, containerRequired, ref hasOuterContainer);

			writer.WriteStartElement(elementName);
			WriteAsMamlContent(reader, writer, false, false, ref hasOuterContainer);
			writer.WriteEndElement();
		}

		private void WriteMamlInlineElementString(XmlReader reader, XmlWriter writer, string elementName, bool containerRequired, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers)
				EnsureContainer(writer, null, containerRequired, ref hasOuterContainer);

			writer.WriteStartElement(elementName);
			writer.WriteString(ReadElementAsString(reader, true));
			writer.WriteEndElement();
		}

		private void EnsureContainer(XmlWriter writer, string startNewContainerName, bool containerRequired, ref bool hasOuterContainer)
		{
			if (!hasOuterContainer && containerRequired)
			{
				writer.WriteStartElement(startNewContainerName ?? DefaultContainerElementName);
				hasOuterContainer = true;
			}
			else if (startNewContainerName != null)
			{
				if (hasOuterContainer)
					writer.WriteEndElement();

				writer.WriteStartElement(startNewContainerName);
				hasOuterContainer = true;
			}
		}

		private void WriteMamlImage(string id, string file, string alternateText, XmlWriter writer)
		{
			writer.WriteStartElement("mediaLink");
			writer.WriteStartElement("image");

			ConceptualImage image = images.GetImage(id, file, alternateText);

			writer.WriteAttributeString("xlink", "href", "http://www.w3.org/1999/xlink", image.Id);
			writer.WriteEndElement();
			writer.WriteEndElement();

			ProcessedImageCount = ProcessedImageCount + 1;
		}

		private void WriteMamlList(XmlReader reader, XmlWriter writer, bool ordered, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers && hasOuterContainer)
				writer.WriteEndElement();

			WriteMamlList(reader, writer, ordered);

			if (canCreateNewContainers)
				hasOuterContainer = false;
		}

		private void WriteMamlList(XmlReader reader, XmlWriter writer, bool ordered)
		{
			writer.WriteStartElement("list");
			writer.WriteAttributeString("class", (ordered) ? "ordered" : "bullet");

			using (XmlReader listReader = reader.ReadSubtree())
			{
				listReader.MoveToContent();

				while (listReader.Read())
				{
					switch (listReader.LocalName)
					{
						case "li":
							writer.WriteStartElement("listItem");

							WriteAsMamlContent(listReader, writer, false);

							writer.WriteEndElement();
							break;
					}
				}
			}

			writer.WriteEndElement();
		}

		private void WriteMamlTable(XmlReader reader, XmlWriter writer, bool canCreateNewContainers, ref bool hasOuterContainer)
		{
			if (canCreateNewContainers && hasOuterContainer)
				writer.WriteEndElement();

			WriteMamlTable(reader, writer);

			if (canCreateNewContainers)
				hasOuterContainer = false;
		}

		private void WriteMamlTable(XmlReader reader, XmlWriter writer)
		{
			string caption;
			List<List<string>> rows = ReadHtmlTable(reader, out caption);

			writer.WriteStartElement("table");

			writer.WriteStartElement("title");
			writer.WriteString(CollapseWhitespace(caption));
			writer.WriteEndElement();

			if (rows.Count > 1)
			{
				WriteMamlTableRow(writer, "tableHeader", rows[0]);
				rows.RemoveAt(0);
			}

			foreach (List<string> row in rows)
				WriteMamlTableRow(writer, "row", row);

			writer.WriteEndElement();
		}

		private static void WriteMamlTableRow(XmlWriter writer, string elementName, List<string> row)
		{
			writer.WriteStartElement(elementName);

			foreach (string column in row)
			{
				writer.WriteStartElement("entry");
				writer.WriteRaw(column);	// column contains the <para> tag
				writer.WriteEndElement();
			}

			writer.WriteEndElement();
		}

		private List<List<string>> ReadHtmlTable(XmlReader reader, out string caption)
		{
			StringBuilder captionBuilder = new StringBuilder();
			List<List<string>> rows = new List<List<string>>();

			using (XmlReader tableReader = reader.ReadSubtree())
			{
				tableReader.MoveToContent();

				while (tableReader.Read())
				{
					if (tableReader.NodeType == XmlNodeType.Element)
					{
						switch (tableReader.LocalName)
						{
							case "caption":
								captionBuilder.AppendLine(ReadElementAsString(tableReader, true));
								break;
							case "tr":
								rows.Add(new List<string>());
								break;
							case "th":	// NOTE: The first row is always used as the header, regardless of the column style.
							case "td":
								if (rows.Count == 0)
									rows.Add(new List<string>());

								List<string> columns = rows[rows.Count - 1];

								columns.Add(ReadAsMamlContent(tableReader, true));
								break;
						}
					}
				}
			}

			caption = captionBuilder.ToString();
			return rows;
		}
		#endregion
	}
}
